<?php
namespace Core\Common\Component\Cache;

use Core\Common\Component\Component;
use Core\Common\Base\Redis;

/**
 * Redis的KV实现
 * Class KvRedis
 * @package App\Component\Cache
 */
class RedisStore extends Component implements CacheStore
{
    const SERIALIZE_TYPE_PHP = 'php';
    const SERIALIZE_TYPE_MSGPACK = 'msgpack';
    const SERIALIZE_TYPE_IGBINARY = 'igbinary';

    /**
     * @var string
     */
    protected $serializeType;

    /**
     * @var bool
     */
    protected $compress;

    protected $redisConfigs;

    /**
     * KvRedis constructor.
     * @param array $configs
     *  e.g. [
     *    "connection" => "",    // 对应实例
     *    "serialize_type" => "igbinary",  // 序列化类型
     *    "compress" => true  // 是否压缩
     * ]
     */
    public function __construct($configs)
    {
        $configs = $configs + [
            'connection' => 'default',
            'serialize_type' => self::SERIALIZE_TYPE_PHP,
            'compress' => false
        ];
        $this->redisConfigs = $configs;
        $this->serializeType = $configs['serialize_type'];
        $this->compress = $configs['compress'];
        parent::__construct($configs);
    }

    /**
     * @return array
     */
    public function __get($name)
    {
        if ($name == 'redis') {
            return Redis::getInstance($this->redisConfigs['connection']);
        }
        return null;
    }

    public function get($key)
    {
        $data = $this->redis->get($key);
        return $data ? $this->unserialize($data) : false;
    }

    public function set($key, $value, $ttl)
    {
        $data = $this->serialize($value);

        if ($ttl >= 0) {
            $ok = $this->redis->setex($key, $ttl, $data);
        } else {
            $ok = $this->redis->set($key, $data);
        }
        return $ok;
    }

    public function mGet($keys)
    {
        if (empty($keys)) {
            return [];
        }

        $result = $this->redis->mget($keys);

        foreach ($result as $i => $data) {
            if ($data) {
                $result[$i] = $this->unserialize($data);
            }
        }
        return $result;
    }

    public function mSet($items, $ttl)
    {
        if (empty($items)) {
            return true;
        }

        $items = array_map(function($item) {
            return $this->serialize($item);
        }, $items);


        if ($ttl >= 0) {
            // redis 没有 msetex 这样的方法，所以遇到有ttl的，只能用pipeline了
            // fixme 这里不应该强依赖\Redis
            $this->redis->multi(\Redis::PIPELINE);
            foreach ($items as $k => $v) {
                $this->redis->set($k, $v, $ttl);
            }
            $result = $this->redis->exec();
            return !in_array(false, $result, true);
        }

        return $this->redis->mset($items);
    }

    public function del($key)
    {
        $this->redis->delete($key);
    }

    public function mDel($keys)
    {
        if (empty($keys)) {
            return;
        }

        $this->redis->delete($keys);
    }

    public function flush()
    {
        //$this->redis->flushDB();
    }

    public function connection()
    {
        return $this->redis;
    }

    /**
     * @param string[] $names
     * @return RedisTaggedCache
     */
    public function tags($names)
    {
        return new RedisTaggedCache($this, $names);
    }

    private function serialize($value)
    {
        if ($this->serializeType == self::SERIALIZE_TYPE_MSGPACK) {
            if (!function_exists('msgpack_pack')) {
                throw new CacheException('msgpack_pack not exists, need msgpack extension.');
            }
            $value = msgpack_pack($value);
        } else if ($this->serializeType == self::SERIALIZE_TYPE_IGBINARY) {
            if (!function_exists('igbinary_serialize')) {
                throw new CacheException('igbinary_serialize not exists, need igbinary extension.');
            }
            $value = igbinary_serialize($value);
        } else {
            $value = serialize($value);
        }

        $needCompress = $this->compress && strlen($value) > 32;
        return $this->compress ? ($needCompress?chr(0xB1):chr(0xB0)).($needCompress?gzdeflate($value):$value) : $value;
    }

    private function unserialize($value)
    {
        if ($this->compress) {
            $data = unpack('c', $value);
            if (($data[1] & 0xf0) != 0xB0) {
                return false;
            }

            $data[2] = substr($value, 1);

            if (($data[1] & 0x01) == 1) {
                $data[2] = gzinflate($data[2]);
                if ($data[1] === false) {
                    return false;
                }
            }

            $value = $data[2];
        }

        try {
            if ($this->serializeType == self::SERIALIZE_TYPE_MSGPACK) {
                if (!function_exists('msgpack_unpack')) {
                    throw new CacheException('msgpack_unpack not exists, need msgpack extension.');
                }
                return msgpack_unpack($value);
            } else if ($this->serializeType == self::SERIALIZE_TYPE_IGBINARY) {
                if (!function_exists('igbinary_unserialize')) {
                    throw new CacheException('igbinary_unserialize not exists, need igbinary extension.');
                }
                return igbinary_unserialize($value);
            } else {
                return unserialize($value);
            }
        } catch (\Exception $e) {
            return false;
        }
    }
}